#include "device/ModbusDevice.h"
#include "core/Logger.h"

#include <algorithm>
#include <chrono>
#include <cctype>
#include <initializer_list>
#include <thread>

namespace {

std::string trimCopy(const std::string& value) {
    size_t start = 0;
    size_t end = value.size();
    while (start < end && std::isspace(static_cast<unsigned char>(value[start]))) {
        ++start;
    }
    while (end > start && std::isspace(static_cast<unsigned char>(value[end - 1]))) {
        --end;
    }
    return value.substr(start, end - start);
}

std::string toLower(const std::string& value) {
    std::string out;
    out.reserve(value.size());
    for (char ch : value) {
        out.push_back(static_cast<char>(std::tolower(static_cast<unsigned char>(ch))));
    }
    return out;
}

std::string toUpper(const std::string& value) {
    std::string out;
    out.reserve(value.size());
    for (char ch : value) {
        out.push_back(static_cast<char>(std::toupper(static_cast<unsigned char>(ch))));
    }
    return out;
}

int toInt(const std::string& value, int defaultValue) {
    if (value.empty()) {
        return defaultValue;
    }
    try {
        return std::stoi(value);
    } catch (...) {
        return defaultValue;
    }
}

bool toBool(const std::string& value, bool defaultValue) {
    if (value.empty()) {
        return defaultValue;
    }
    std::string lower = toLower(value);
    if (lower == "true" || lower == "1" || lower == "yes" || lower == "on") {
        return true;
    }
    if (lower == "false" || lower == "0" || lower == "no" || lower == "off") {
        return false;
    }
    return defaultValue;
}

} // namespace

namespace ems {
namespace device {

ModbusDevice::ModbusDevice(const std::string& id, const std::string& name, const std::string& type)
    : DeviceBase(id, name, type) {
    protocolManager_ = std::unique_ptr<protocol::ProtocolManager>(new protocol::ProtocolManager());
}

ModbusDevice::~ModbusDevice() {
    disconnect();
}

bool ModbusDevice::initialize() {
    LOG_INFO("Initializing Modbus device: " + name_);

    if (protocolConfigPath_.empty()) {
        setLastError("Protocol config path not set");
        return false;
    }

    if (!loadProtocolConfig()) {
        setLastError("Failed to load protocol config");
        return false;
    }

    LOG_INFO("Modbus device initialized: " + name_);
    return true;
}

bool ModbusDevice::connect() {
    if (isConnected()) {
        return true;
    }

    setStatus(DeviceStatus::CONNECTING);

    if (connectionParams_.isRtu) {
        LOG_INFO("Connecting to Modbus RTU device: " + name_ + " via " + connectionParams_.rtu.device);

        if (!rtuClient_) {
            rtuClient_ = std::unique_ptr<protocol::ModbusRTU>(new protocol::ModbusRTU(
                connectionParams_.rtu.device,
                connectionParams_.rtu.baudRate,
                connectionParams_.rtu.parity,
                connectionParams_.rtu.dataBits,
                connectionParams_.rtu.stopBits
            ));
        }

        if (connectionParams_.rtu.connectionDelay > 0) {
            std::this_thread::sleep_for(std::chrono::milliseconds(connectionParams_.rtu.connectionDelay));
        }

        if (!rtuClient_->connect()) {
            setStatus(DeviceStatus::ERROR);
            setLastError("Failed to connect to Modbus RTU device");
            LOG_ERROR("Failed to connect to Modbus RTU device: " + name_ + ": " + rtuClient_->getLastError());
            return false;
        }

        rtuClient_->setSlaveId(connectionParams_.slaveId);
        rtuClient_->setTimeout(connectionParams_.timeout);
    } else {
        LOG_INFO("Connecting to Modbus TCP device: " + name_ + " at " +
                 connectionParams_.address + ":" + std::to_string(connectionParams_.port));

        if (!tcpClient_) {
            tcpClient_ = std::unique_ptr<protocol::ModbusTCP>(new protocol::ModbusTCP(
                connectionParams_.address,
                connectionParams_.port
            ));
        }

        if (!tcpClient_->connect()) {
            setStatus(DeviceStatus::ERROR);
            setLastError("Failed to connect to Modbus TCP device");
            LOG_ERROR("Failed to connect to Modbus TCP device: " + name_ + ": " + tcpClient_->getLastError());
            return false;
        }

        tcpClient_->setSlaveId(connectionParams_.slaveId);
        tcpClient_->setTimeout(connectionParams_.timeout);
    }

    setStatus(DeviceStatus::CONNECTED);
    LOG_INFO("Successfully connected to Modbus device: " + name_);
    return true;
}

void ModbusDevice::disconnect() {
    if (tcpClient_) {
        tcpClient_->disconnect();
    }
    if (rtuClient_) {
        rtuClient_->disconnect();
    }
    setStatus(DeviceStatus::DISCONNECTED);
    LOG_INFO("Disconnected from Modbus device: " + name_);
}

bool ModbusDevice::isConnected() const {
    if (connectionParams_.isRtu) {
        return rtuClient_ && rtuClient_->isConnected();
    }
    return tcpClient_ && tcpClient_->isConnected();
}

bool ModbusDevice::collectData(std::vector<DataValue>& data) {
    if (!isConnected()) {
        LOG_WARN("Modbus device not connected, attempting to reconnect: " + name_);
        if (!connect()) {
            setLastError("Device not connected");
            return false;
        }
    }

    if (!enabled_) {
        return true; // Skip disabled devices
    }

    LOG_DEBUG("Collecting data from device: " + name_);

    data.clear();
    bool success = readDataPoints(data);

    if (success) {
        lastCollectionTime_ = getCurrentTimestamp();
        LOG_INFO("Successfully collected " + std::to_string(data.size()) + " data points from: " + name_);

        if (!data.empty()) {
            LOG_INFO("=== DATA COLLECTION RESULT ===");
            LOG_INFO("Device: " + name_ + " (ID: " + id_ + ")");
            LOG_INFO("Timestamp: " + std::to_string(lastCollectionTime_));
            for (size_t i = 0; i < data.size(); ++i) {
                const auto& item = data[i];
                LOG_INFO("  [" + std::to_string(i + 1) + "] " + item.key + " = " +
                         std::to_string(item.value) +
                         (item.unit.empty() ? "" : " " + item.unit) +
                         " (valid: " + (item.valid ? "true" : "false") +
                         ", category: " + item.deviceCategory + ")");
            }
            LOG_INFO("=============================");
        }
    } else {
        setLastError("Failed to collect data");
        LOG_ERROR("Failed to collect data from device: " + name_);
    }

    return success;
}

bool ModbusDevice::writeData(const std::string& key, double value) {
    if (!isConnected()) {
        LOG_WARN("Modbus device not connected, attempting to reconnect before write: " + name_);
        if (!connect()) {
            setLastError("Device not connected");
            return false;
        }
    }

    protocol::ProtocolInterface* client = getClient();
    if (!client) {
        setLastError("Modbus client not initialized");
        return false;
    }

    const auto* dataPoint = protocolManager_->getDataPoint(key);
    if (!dataPoint) {
        setLastError("Data point not found: " + key);
        return false;
    }

    uint16_t rawValue = static_cast<uint16_t>(value / dataPoint->coefficient);

    bool success = false;
    switch (dataPoint->registerType) {
        case protocol::RegisterType::HOLDING_REGISTER:
            success = client->writeSingleRegister(dataPoint->address, rawValue);
            break;
        case protocol::RegisterType::COIL:
            success = client->writeSingleCoil(dataPoint->address, rawValue != 0);
            break;
        default:
            setLastError("Unsupported register type for writing");
            return false;
    }

    if (!success) {
        setLastError("Failed to write data to register: " + std::to_string(dataPoint->address));
        LOG_ERROR("Failed to write data to device: " + name_ + ", register: " + std::to_string(dataPoint->address));
    }

    return success;
}

bool ModbusDevice::setConnectionParams(const utils::DeviceConfig& config) {
    ConnectionParams params;

    std::string protoLower = toLower(config.protocol);
    params.isRtu = (protoLower == "modbus_rtu" || protoLower == "rtumodbus");

    auto getConnValue = [&](std::initializer_list<const char*> keys) -> std::string {
        for (const char* key : keys) {
            auto it = config.connection.find(key);
            if (it != config.connection.end() && !it->second.empty()) {
                return trimCopy(it->second);
            }
        }
        return std::string();
    };

    params.slaveId = toInt(getConnValue({"slave_id", "slave", "id"}), 1);
    params.timeout = toInt(getConnValue({"timeout", "response_timeout"}), 3000);

    if (params.isRtu) {
        params.rtu.device = getConnValue({"address", "device", "serial_port"});
        if (params.rtu.device.empty()) {
            setLastError("RTU serial device is required");
            LOG_ERROR("RTU serial device not provided for Modbus device: " + name_);
            return false;
        }

        params.address = params.rtu.device;
        params.rtu.serialType = getConnValue({"serial_type"});
        if (params.rtu.serialType.empty()) {
            params.rtu.serialType = "rtu";
        }

        params.rtu.baudRate = toInt(getConnValue({"baud_rate", "baud", "baudrate"}), 9600);
        params.rtu.dataBits = toInt(getConnValue({"data_bits", "databits"}), 8);
        params.rtu.stopBits = toInt(getConnValue({"stop_bits", "stopbits"}), 1);

        std::string parityStr = toUpper(getConnValue({"parity"}));
        if (!parityStr.empty()) {
            params.rtu.parity = parityStr[0];
        }

        params.rtu.connectionDelay = toInt(getConnValue({"connection_delay", "connect_delay"}), 0);
        params.rtu.reconnectOnTimeout = toBool(getConnValue({"reconnect_on_timeout"}), true);
        params.rtu.reconnectTimeout = toInt(getConnValue({"reconnect_timeout"}), 2000);

        if (params.rtu.baudRate <= 0) {
            params.rtu.baudRate = 9600;
        }
        if (params.rtu.dataBits < 5 || params.rtu.dataBits > 8) {
            params.rtu.dataBits = 8;
        }
        if (params.rtu.stopBits < 1 || params.rtu.stopBits > 2) {
            params.rtu.stopBits = 1;
        }
        char parityUpper = static_cast<char>(std::toupper(static_cast<unsigned char>(params.rtu.parity)));
        if (parityUpper != 'N' && parityUpper != 'E' && parityUpper != 'O') {
            parityUpper = 'N';
        }
        params.rtu.parity = parityUpper;

        LOG_INFO("Configured Modbus RTU device " + name_ +
                 " - serial=" + params.rtu.device +
                 ", baud=" + std::to_string(params.rtu.baudRate) +
                 ", parity=" + std::string(1, params.rtu.parity) +
                 ", dataBits=" + std::to_string(params.rtu.dataBits) +
                 ", stopBits=" + std::to_string(params.rtu.stopBits) +
                 ", slaveId=" + std::to_string(params.slaveId));
    } else {
        params.address = getConnValue({"address", "ip", "host"});
        if (params.address.empty()) {
            params.address = "127.0.0.1";
        }
        params.port = toInt(getConnValue({"port"}), 502);

        LOG_INFO("Configured Modbus TCP device " + name_ +
                 " - " + params.address + ":" + std::to_string(params.port) +
                 ", slaveId=" + std::to_string(params.slaveId));
    }

    connectionParams_ = params;
    resetClient();
    return true;
}

bool ModbusDevice::setProtocolConfig(const std::string& protocolConfigPath) {
    protocolConfigPath_ = protocolConfigPath;
    LOG_INFO("Set protocol config path for device: " + name_ + " - " + protocolConfigPath);
    return true;
}

bool ModbusDevice::loadProtocolConfig() {
    if (!protocolManager_->loadProtocol(protocolConfigPath_)) {
        LOG_ERROR("Failed to load protocol config: " + protocolConfigPath_);
        return false;
    }

    LOG_INFO("Loaded protocol config for device: " + name_ + " from: " + protocolConfigPath_);
    return true;
}

bool ModbusDevice::readDataPoints(std::vector<DataValue>& data) {
    const auto& config = protocolManager_->getConfig();
    bool allSuccess = true;

    for (const auto& dataPoint : config.dataPoints) {
        DataValue dataValue;
        if (readDataPoint(dataPoint, dataValue)) {
            data.push_back(dataValue);
            LOG_DEBUG("Successfully read data point: " + dataPoint.name + " = " +
                      std::to_string(dataValue.value) +
                      (dataValue.unit.empty() ? "" : " " + dataValue.unit) +
                      " (address: " + std::to_string(dataPoint.address) + ")");
        } else {
            allSuccess = false;
            LOG_WARN("Failed to read data point: " + dataPoint.name + " from device: " + name_);
        }
    }

    return allSuccess;
}

bool ModbusDevice::readDataPoint(const protocol::DataPoint& dataPoint, DataValue& dataValue) {
    protocol::ProtocolInterface* client = getClient();
    if (!client) {
        setLastError("Modbus client not initialized");
        return false;
    }

    std::vector<uint16_t> rawData;
    bool success = false;
    auto registerTypeToString = [](protocol::RegisterType type) -> const char* {
        switch (type) {
            case protocol::RegisterType::HOLDING_REGISTER: return "holding_register";
            case protocol::RegisterType::INPUT_REGISTER: return "input_register";
            case protocol::RegisterType::COIL: return "coil";
            case protocol::RegisterType::DISCRETE_INPUT: return "discrete_input";
            default: return "unknown";
        }
    };

    LOG_DEBUG("Reading data point from device " + name_ +
              " [point=" + dataPoint.name +
              ", type=" + registerTypeToString(dataPoint.registerType) +
              ", address=" + std::to_string(dataPoint.address) +
              ", length=" + std::to_string(dataPoint.length) + "]");

    switch (dataPoint.registerType) {
        case protocol::RegisterType::HOLDING_REGISTER:
            success = client->readHoldingRegisters(dataPoint.address, dataPoint.length, rawData);
            break;
        case protocol::RegisterType::INPUT_REGISTER:
            success = client->readInputRegisters(dataPoint.address, dataPoint.length, rawData);
            break;
        case protocol::RegisterType::COIL:
        {
            std::vector<bool> coils;
            success = client->readCoils(dataPoint.address, dataPoint.length, coils);
            if (success && !coils.empty()) {
                rawData.clear();
                rawData.reserve(coils.size());
                for (bool coil : coils) {
                    rawData.push_back(coil ? 1 : 0);
                }
            }
            break;
        }
        case protocol::RegisterType::DISCRETE_INPUT:
        {
            std::vector<bool> inputs;
            success = client->readDiscreteInputs(dataPoint.address, dataPoint.length, inputs);
            if (success && !inputs.empty()) {
                rawData.clear();
                rawData.reserve(inputs.size());
                for (bool input : inputs) {
                    rawData.push_back(input ? 1 : 0);
                }
            }
            break;
        }
        default:
            LOG_ERROR("Unsupported register type: " + std::to_string(static_cast<int>(dataPoint.registerType)));
            return false;
    }

    if (!success) {
        LOG_ERROR("Failed to read Modbus data from device: " << name_
                  << ", point=" << dataPoint.name
                  << ", type=" << registerTypeToString(dataPoint.registerType)
                  << ", address=" << dataPoint.address
                  << ", length=" << dataPoint.length
                  << ", error=" << client->getLastError());
        return false;
    }

    if (rawData.empty()) {
        LOG_ERROR("Empty data received from device: " << name_
                  << ", point=" << dataPoint.name
                  << ", type=" << registerTypeToString(dataPoint.registerType)
                  << ", address=" << dataPoint.address);
        return false;
    }

    double convertedValue = convertRawData(dataPoint, rawData);

    dataValue.key = dataPoint.name;
    dataValue.value = convertedValue;
    dataValue.unit = dataPoint.unit;
    dataValue.timestamp = getCurrentTimestamp();
    dataValue.valid = true;
    dataValue.deviceCategory = dataPoint.deviceCategory;

    return true;
}

double ModbusDevice::convertRawData(const protocol::DataPoint& dataPoint, const std::vector<uint16_t>& rawData) {
    if (rawData.empty()) {
        return 0.0;
    }

    double rawValue = 0.0;

    switch (dataPoint.dataType) {
        case protocol::DataType::UINT16:
            rawValue = static_cast<double>(rawData[0]);
            break;
        case protocol::DataType::INT16:
            rawValue = static_cast<double>(static_cast<int16_t>(rawData[0]));
            break;
        case protocol::DataType::UINT32:
            if (rawData.size() >= 2) {
                uint32_t value = (static_cast<uint32_t>(rawData[0]) << 16) | rawData[1];
                rawValue = static_cast<double>(value);
            }
            break;
        case protocol::DataType::INT32:
            if (rawData.size() >= 2) {
                int32_t value = (static_cast<int32_t>(rawData[0]) << 16) | rawData[1];
                rawValue = static_cast<double>(value);
            }
            break;
        case protocol::DataType::FLOAT32:
            if (rawData.size() >= 2) {
                union {
                    float f;
                    uint32_t u;
                } converter;
                converter.u = (static_cast<uint32_t>(rawData[0]) << 16) | rawData[1];
                rawValue = static_cast<double>(converter.f);
            }
            break;
        default:
            LOG_WARN("Unsupported data type for conversion: " + std::to_string(static_cast<int>(dataPoint.dataType)));
            rawValue = static_cast<double>(rawData[0]);
            break;
    }

    double convertedValue = rawValue * dataPoint.coefficient + dataPoint.offset;

    if (dataPoint.minValue != dataPoint.maxValue) {
        convertedValue = std::max(dataPoint.minValue, std::min(dataPoint.maxValue, convertedValue));
    }

    return convertedValue;
}

protocol::RegisterType ModbusDevice::getFunctionCodeType(int functionCode) {
    switch (functionCode) {
        case 1: return protocol::RegisterType::COIL;
        case 2: return protocol::RegisterType::DISCRETE_INPUT;
        case 3: return protocol::RegisterType::HOLDING_REGISTER;
        case 4: return protocol::RegisterType::INPUT_REGISTER;
        default: return protocol::RegisterType::HOLDING_REGISTER;
    }
}

protocol::ProtocolInterface* ModbusDevice::getClient() {
    if (connectionParams_.isRtu) {
        return rtuClient_.get();
    }
    return tcpClient_.get();
}

const protocol::ProtocolInterface* ModbusDevice::getClient() const {
    if (connectionParams_.isRtu) {
        return rtuClient_.get();
    }
    return tcpClient_.get();
}

void ModbusDevice::resetClient() {
    tcpClient_.reset();
    rtuClient_.reset();
}

}  // namespace device
}  // namespace ems
